import { AObject } from "./AObject";
import { Agent } from "./Agent";
import { parseNumber } from "./CommandParser";
import * as Debug from "./Debug";
import { PronounNames } from "./NarrativeBase";
import type { NarrativeContext } from "./NarrativeContext";
import { AttributeRegistry, ClassRegistry } from "./Primitives";
import { U } from "./Util";

export class NLPError extends Error {
    constructor(msg: 'syntax' | 'logic' | 'prohibited' | 'unclear', 
        public expr: string, 
        public detail: string = '') 
    {
        super(msg);
        this.name = 'NLPError'
    }
}

export type SelectorYield = {
    result: 'end'
} | {
    result: 'subphraseEnd',
    notEnough: boolean,
    original: string
} | {
    result: 'ambiguous',
    original: string,
    candidates: AObject[]
} | {
    result: 'containerAmbiguous',
    original: string,
    candidates: AObject[]
} | {
    result: 'ok',
    index: number,
    number: number,
    original: string,
    object: AObject,
} | {
    result: 'noSuchObject',
    original: string
} | {
    result: 'conditionNotMet',
    original: string,
    object: AObject
} | {
    result: 'containerNotFound',
    original: string
}

/**
 * @param pred Predicate for basic relevence of object.
 * @param subObjPred Predicate for whether to include children of object. Note that contents must be visible (`obj.canSeeContents == true`) to be included in any way.
 * @param conditions A list of predicates indicating detailed conditions about whether the object if relevent.
 * @param [includeThis=true] Set this to `false` if you only want to search the children of objects in `range`.
 * @returns an `AObject[]` if anything found. If not, returns the last AObject (if any) that matches the predicate but not the condition.
 */
function searchForIn(
    range: AObject[], pred: (obj: AObject) => boolean,
    subObjPred: (obj: AObject) => boolean, 
    condition?: (obj: AObject) => boolean,
    includeThis = true): AObject[] | AObject | null
{
    let candidates: AObject[] = [];
    let lastObject: AObject | null = null;
    // loop through range; keep advancing one "depth-layer" if not found
    while (true) {
        let next_range: AObject[] = [];
        for (let obj of range) {
            if (pred(obj) && includeThis) {
                lastObject = obj;
                if (!condition || condition(obj))
                    candidates.push(obj);
            }
            if (!subObjPred(obj)) continue;
            next_range.push(...obj.parts);
            if (obj.canSeeContents)
                next_range.push(...obj.contents)
        }
        if (candidates.length != 0 || next_range.length == 0) break;
        range = next_range;
        includeThis = true;
    }
    if (candidates.length > 0) return candidates;
    return lastObject;
}

export type ObjectSelectorPolicy = {
    yieldSubobjectOfVisited: boolean
}

export class ObjectSelector {
    #getRange: (() => AObject[]) | null = null;
    #condition?: (obj: AObject) => boolean;
    #visited = new Set<AObject>;
    #index = 0;

    #contSelectorStack: ObjectSelectorPart[] = []
    #contSelectorDisambiguation?: AObject | null;

    policy: ObjectSelectorPolicy = {
        yieldSubobjectOfVisited: true
    }

    static get everything() {
        return new ObjectSelector([{number: -1, 
            part: new PredicateObjectSelector((x) => true, '', '<everything>')}])
    }

    static get anything() {
        return new ObjectSelector([{number: null, 
            part: new PredicateObjectSelector((x) => true, '', '<everything>')}])
    }

    constructor(
        private _list: {number: number | null, part: ObjectSelectorPart}[])
    {}

    get isSingular() {
        return this._list.length == 1 && (this._list[0].number ?? 1) == 1;
    }

    get debugString() {
        let det = (n: number | null) => {
            if (n == null) return '(nodet)';
            if (n < 0) return '(all)';
            return `(${n})×`
        }
        return this._list.map((x) => det(x.number) + x.part.debugString).join('; ');
    }

    get isInitialized() {
        return this.#getRange != null;
    }

    submitContainerSelectorDisambiguation(obj: AObject | null) {
        Debug.assert(this.#contSelectorStack.length > 0);
        Debug.assert(this.#contSelectorDisambiguation === undefined);
        this.#contSelectorDisambiguation = obj;
    }

    initialize(
        getRange: () => AObject[], 
        condition: (obj: AObject) => boolean)
    {
        this.#getRange = getRange;
        this.#condition = condition;
    }

    #findContainer(range: AObject[], level: number = 0): AObject | SelectorYield {
        Debug.assert(this.#getRange != null); // initialized
        Debug.assert(this.#contSelectorStack.length > level);
        Debug.assert(level < 6);

        let selector = this.#contSelectorStack[level];
        if ('next' in selector) {
            // ADV_TODO: ensure only singular
            Debug.assert(selector.referents.length == 1);
            // ADV_TODO referring to part of agent
            Debug.assert(selector.referents[0] instanceof AObject);
            this.#contSelectorStack.pop();
            return selector.referents[0];
        }

        // selector is predicate
        if (level == this.#contSelectorStack.length - 1 
            && this.#contSelectorDisambiguation !== undefined)
        {
            this.#contSelectorStack.pop();
            if (this.#contSelectorDisambiguation)
                return this.#contSelectorDisambiguation;
            else
                return {result: 'containerNotFound', original: selector.originalString};
        }

        let output: ReturnType<typeof searchForIn>;
        if (selector.containerSelector != null) {
            if (this.#contSelectorStack.length < level + 1)
                this.#contSelectorStack.push(selector.containerSelector);
            let cont = this.#findContainer(range, level + 1);
            if (!(cont instanceof AObject)) return cont;
            output = searchForIn([cont], selector.predicate, 
                () => true, (x) => x != cont);
        } else
            output = searchForIn(range, selector.predicate, (_) => true);
        
        Debug.assert(level == this.#contSelectorStack.length - 1);
        if (Array.isArray(output)) {
            if (output.length == 1) {
                // success
                this.#contSelectorStack.pop();
                return output[0];
            } else // keep stack status
                return { result: 'containerAmbiguous', 
                    original: selector.originalString, candidates: output};
        } else {
            // whole stack fails
            this.#contSelectorStack = [];
            return {result: 'containerNotFound', original: selector.originalString};
        }
    }

    yieldNext(): SelectorYield {
        if (this._list.length == 0) return {result: 'end'};
        let entry = this._list[0];

        if ('next' in entry.part) {
            // ADV_TODO referring to part of agent
            let next = entry.part.next();
            if (!next) {
                this._list.splice(0, 1);
                return {
                    result: 'subphraseEnd', 
                    notEnough: false,
                    original: entry.part.originalString};
            }

            Debug.assert(next instanceof AObject);
            return {result: 'ok', object: next, 
                index: entry.part.index, 
                number: entry.part.referents.length, 
                original: entry.part.originalString}
        }

        let selector = entry.part;
        // generate range and search from bottom every time
        Debug.assert(this.#getRange != null);
        let range = this.#getRange();
        let includeThis = true;
        if (selector.containerSelector != null) {
            if (this.#contSelectorStack.length == 0)
                this.#contSelectorStack.push(selector.containerSelector);
            let ret = this.#findContainer(range);
            if (ret instanceof AObject) {
                // focus on the children of this object only
                range = [ret];
                includeThis = false;
            } else
                // do not advance, of course
                return ret;
        }

        let output = searchForIn(range, 
            (obj) => !this.#visited.has(obj) && selector.predicate(obj),
            (obj) => this.policy.yieldSubobjectOfVisited || !this.#visited.has(obj),
            this.#condition, includeThis);

        let alldone = false; // whether to move on to next phrase
        let result: SelectorYield | null = null;

        if (this.#index == (entry.number ?? 1)) {
            alldone = true;
            result = {
                result: 'subphraseEnd', 
                notEnough: false,
                original: selector.originalString};
        } else if (!Array.isArray(output)){
            // no available object.
            if (this.#index == 0) {
                result = output == null
                    ? { result: 'noSuchObject', 
                        original: selector.originalString } 
                    : { result: 'conditionNotMet',
                        original: selector.originalString,
                        object: output};
            } else {
                // except when taking all of something, arriving at this point means we 
                // haven't found enough of them
                result = {
                    result: 'subphraseEnd', 
                    notEnough: entry.number != -1,
                    original: selector.originalString};
            }
            alldone = true;
        } else if (entry.number == null) {
            // no determiner (in Chinese = definitive article)
            let list = output as AObject[];
            if (output.length == 1) this.#visited.add(output[0]);
            result = output.length == 1
                ? {result: 'ok', index: 0, number: 1, 
                    object: list[0], 
                    original: selector.originalString}
                : {result: 'ambiguous', 
                    candidates: list, 
                    original: selector.originalString};

            this.#index += 1;
        } else {
            // taking all or a specific number (randomly)
            let object = U.choice(...output as AObject[]);
            this.#visited.add(object);
            result = {
                result: 'ok', index: this.#index, 
                number: entry.number, object: object,
                original: selector.originalString};

            this.#index += 1;
        }

        if (alldone) {
            this._list.splice(0, 1);
            this.#index = 0;
        }
        return result;
    }
}

type ObjectSelectorPart = PredicateObjectSelector | ConstantObjectSelector;

class PredicateObjectSelector {
    constructor(
        public predicate: (x: AObject) => boolean,
        public readonly originalString: string,
        public debugString: string) {}

    public containerSelector: PredicateObjectSelector | null = null;
}

class ConstantObjectSelector {
    #i = 0;
    get index() {return this.#i;}

    constructor(public referents: AObject[], public originalString: string) {}

    next(): AObject | null {
        if (this.#i >= this.referents.length) return null;
        let obj = this.referents[this.#i];
        this.#i++;
        return obj;
    }

    get debugString() {
        return `<${this.originalString}=${this.referents.map((x) => x.name.value).join(';')}>`;
    }
}

export type ParseOptions = {
    allowAll?: boolean,
    restrictSingular?: boolean,
    context?: NarrativeContext
}

// ObjectSelector: multiple filters
//      := REF [(、|，) REF]* [[，] (以及|和|还有) REF]
//
//  REF: filter or pronoun
//      := QPHR | PRON
// QPHR: a single filter, perhaps quantified and/or with container specification
//      := [QPHR (上的|里的)] [(所有|一切|每 MEASURE)｜([任意|随机|随便] NUMBER MEASURE)] NPHR
// NPHR: a single filter = adjectival + name
//      := (APHR (的 NAME|东西)｜NAME)
// APHR: adjectival filter
//      := ADJ [(的|且|但|然而|却) ADJ]*
//
// MEASURE := 个|只|张|颗|本|扇|条|把|根|头|支|件|架
//    PRON := 她|他|它|它们
//
// Whenever possible, adjectives are interpreted as attributes and names as classes.

export function parseObjectSelector(
    s: string, options: ParseOptions = {}): ObjectSelector
{
    let m = /^(.+?)(?:(、|，)(.+?))*(?:，?(以及|和|还有)(.+))?$/.exec(s);
    if (m == null) throw new NLPError('syntax', s);

    let list = s.split(/、|，?(?:以及|和|还有)/);
    if (list.length > 1 && options.restrictSingular)
        throw new NLPError('prohibited', 'multiple');
    let number: number | null = null;
    let phrs = list.map((subphr) => {
        let pron = parsePronoun(subphr, options);
        if (pron) return {number: -1, part: pron};

        let q = parseQPHR(subphr, options);
        if (q == null) {
            // ADV_FIXME: using null to mark 'everything' is very awkward
            if (list.length > 1)
                throw new NLPError('logic', s);
            else if (!options.allowAll)
                throw new NLPError('prohibited', s, 'all');
            return { number: -1, 
                part: new PredicateObjectSelector((x) => true, subphr, '<everything>')}
        }
        // if no determiner then carry on previous one
        if (q.number == null) q.number = number;
        else number = q.number;
        return q;
    })
    return new ObjectSelector(phrs);
}

function parsePronoun(s: string, options: ParseOptions = {}) {
    if (!options.context) return null;
    if (s.match(/^(她|他|它|它们)$/)) {
        let pron = PronounNames[s as '她'|'他'|'它'|'它们'];
        let query = options.context.queryPronoun(pron);
        if (!query) {
            throw new NLPError('unclear', s);
        }
        if (Array.isArray(query)) {
            let objs = query.filter((x) => x instanceof AObject) as AObject[];
            return new ConstantObjectSelector(objs, s);
        } else {
            if (query instanceof Agent) return null;
            return new ConstantObjectSelector([query], s);
        }
    }
    return null;
}

function parseQPHR(s: string, options: ParseOptions = {})
{
    const detPostfix = '(?:个|只|张|颗|本|扇|条|把|根|头|支|件|架)';
    let m = RegExp(`^(?:(.+)(?:上|里)的?)?(?:(所有|一切|每${detPostfix})|(任意|随机|随便)?(.+?)${detPostfix})?(.+)$`).exec(s);
    if (m == null) throw new NLPError('syntax', s);

    let num: number | null = null;
    let container: PredicateObjectSelector | null = null;
    if (m[1] != undefined) {
        // read positional clause
        let opt: ParseOptions = {...options, restrictSingular: true };
        let result = parseQPHR(m[1], opt);
        if (result == null) return null; // shouldn't happen?
        container = result.part;
    }
    if (options.restrictSingular && (m[2] ?? m[3]) != undefined)
        throw new NLPError('prohibited', 'multiple');

    if (m[4] != undefined) {
        // read number
        num = parseNumber(m[4]);
        if (isNaN(num) || num == 0) throw new NLPError('syntax', s);
        if (options.restrictSingular && num != 1)
            throw new NLPError('prohibited', s, 'multiple');
    } else if (m[2] != undefined)
        num = 1; // any of
    else if (m[3] != undefined)
        num = -1; // all of

    let phr = parseNPHR(m[5]);
    if (phr == null) return null;
    phr.containerSelector = container;
    if (container != null)
        phr.debugString += 'in:[' + container?.debugString + ']'
    return {number: num, part: phr};
}

function parseNPHR(s: string): PredicateObjectSelector | null {
    let m = /^(?:(.+)(?:的(.+?)|东西)|(.+))$/.exec(s);
    if (m == null) throw new NLPError('syntax', s);

    let aphr = m[1] != undefined ? parseAPHR1(m[1]) : null;
    let name = m[2] ?? m[3];

    if (name == undefined || name == '东西') {
        if (aphr == null) return null;
        return new PredicateObjectSelector(aphr.predicate,
            s, aphr?.debugString ?? '' + `<anything>`);
    }

    let classID = ClassRegistry.get(name);
    let attrId = AttributeRegistry.get(name);
    let string = aphr?.debugString ?? '' + `<'${name}'>`;
    let predicate = (x: AObject) => 
        (x.name.matches(name)) && (aphr == null || aphr.predicate(x));
    if (classID != null) {
        predicate = (x: AObject) => {
            Debug.assert(classID != null);
            return (x.class?.matchesExtended(classID) ?? false) 
                && (aphr == null || aphr.predicate(x));
        };
        string = aphr?.debugString ?? '' + `<class:${classID.name}>`;
    } else if (attrId != null) {
        predicate = (x: AObject) => {
            Debug.assert(attrId != null);
            if (!x.attribute.has(attrId)) return false;
            return aphr == null || aphr.predicate(x);
        };
        string = aphr?.debugString ?? '' + `<attr:${attrId.value}>`;
    }
    return new PredicateObjectSelector(predicate, s, string);
}

function parseAPHR1(s: string): PredicateObjectSelector {
    let list = s.split(/或者|或/).filter((x) => x.length > 0);
    let mapped = list.map(parseAPHR2);
    Debug.assert(mapped.length > 0);

    let predicate = (x: AObject) => {
        for (let i of mapped)
            if (i.predicate(x)) return true;
        return false;
    };
    return new PredicateObjectSelector(predicate, s,
        'attr:' + (list.length < 2 ? list[0] : '[' + list.join('|') + ']'));
}

function parseAPHR2(s: string): PredicateObjectSelector {
    let list = s.split(/的|且|但|然而|却/).filter((x) => x.length > 0);
    Debug.assert(list.length > 0);
    let predicate = (x: AObject) => {
        for (let i of list) {
            let ok = false
            for (let attr of x.attribute)
                if (attr.matches(i)) {
                    ok = true;
                    break;
                }
            if (!ok) return false;
        }
        return true;
    };
    return new PredicateObjectSelector(predicate, s,
        list.length < 2 ? list[0] : '[' + list.join('&') + ']');
}